﻿using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Security.Cryptography;

namespace GoFishWeb.Models
{
    public class GoFishGame
    {
        protected GoFishGame()
        {
            // needed for entity framework.  Must be no parameters and can not be private
        }

        public GoFishGame(string gameName)
        {
            Moves = new List<GoFishMove>();
            Books = new List<GoFishBook>();
            Players = new List<GoFishPlayer>();
            Deck = new Deck();
            Name = gameName;
            LogMove("Game created.");
        }

        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }

        public string Name { get; set; }

        [Required]
        public bool IsGameStarted { get; set; }

        [Required]
        public bool IsGameOver { get; set; }

        public virtual Deck Deck { get; set; }

        public virtual List<GoFishPlayer> Players { get; set; }

        public virtual ICollection<GoFishBook> Books { get; set; }

        public virtual List<GoFishMove> Moves { get; set; }

        public virtual GoFishPlayer CurrentPlayer { get; set; }

        public virtual PlayerRecord PlayerRecord { get; set; }
        
        public void AddPlayer(string playerName)
        {
            // Can't add players to a game that's already started
            if (IsGameStarted || IsGameOver) return;
                
            // Already a player.  Don't need to add them again.  Precents duplicate moves in the log
            if (Players.Any(p => p.Name.Equals(playerName))) return;
            
            Players.Add(new GoFishPlayer(playerName));
            LogMove(string.Format("{0} joined the game.", playerName));
        }

        public virtual bool IsTimeOut { get; set; }

        /// <summary>
        /// </summary>
        /// <param name="playerName"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException">If playername is not a valid player in this game</exception>
        public GoFishPlayer GetPlayer(string playerName)
        {
            var player = Players.FirstOrDefault(x => x.Name == playerName);
            if (player == null) throw new ArgumentException(string.Format("{0} is not a valid player.", playerName));
            return player;
        }

        public int GetPlayerCount()
        {
            return Players.Count();
        }

        /// <summary>
        /// </summary>
        /// <param name="playerName"></param>
        /// <returns>True if the player name matches the current player, false if not</returns>
        public bool IsCurrentPlayer(string playerName)
        {
            return CurrentPlayer.Name.Equals(playerName);
        }

        /// <summary>
        ///     Starts the game if it can be started.
        ///     Shuffles the Deck
        ///     Deals cards for players hands
        ///     Pulls out all books from players hands
        ///     Sets current player to first player to the game creator
        /// </summary>
        /// <exception cref="InvalidOperationException">If less than three players are in the game</exception>
        /// ///
        /// <exception cref="InvalidOperationException">If the game has already been started</exception>
        /// ///
        /// <exception cref="InvalidOperationException">If the game is already finished</exception>
        public void StartGame()
        {
            if (GetPlayerCount() < 3)
                throw new InvalidOperationException("The game may not be started with fewer than 3 players.");

            if (IsGameStarted)
                throw new InvalidOperationException("The game has already been started.");

            if (IsGameOver)
                throw new InvalidOperationException(string.Format("The game has already finished"));
            
            IsGameStarted = true;
            LogMove("Game started.");

            Deck.InitializeDeck();
            Deck.Shuffle();
            Deal();

            foreach (var player in Players)
                PullOutBooks(player);

            CurrentPlayer = Players[0];
        }

        /// <summary>
        /// </summary>
        /// <param name="requestedRank"></param>
        /// <param name="askingPlayerName"></param>
        /// <param name="askedPlayerName"></param>
        /// <returns>True if the asking player received cards, False if they did not </returns>
        /// <exception cref="ArgumentException">
        ///     If requestedRank is not valid
        ///     If the asking player does not have a card of the requested rank
        ///     If either playername is invalid
        /// </exception>
        public bool AskForCard(string requestedRank, string askingPlayerName, string askedPlayerName)
        {
            var askingPlayer = GetPlayer(askingPlayerName);

            var rank = (CardRank)Enum.Parse(typeof(CardRank), ToUpperFirstLetter(requestedRank.ToLower()));

            if (askingPlayer.Hand.All(card => card.Rank != rank))
                throw new ArgumentException(
                    string.Format("You must have a card of rank {0} to ask for cards of that rank.", rank));

            var askedPlayer = GetPlayer(askedPlayerName);
            
            LogMove(string.Format("{0} asked {1} for {2}",
                                askingPlayer.Name, 
                                askedPlayer.Name,
                                Card.Plural(rank)));

            var cardsToTake = askedPlayer.TakeCards(rank).ToList();
            var receivedCards = cardsToTake.Any();

            if (receivedCards)
            {
                askingPlayer.AddCardsToHand(cardsToTake);
                LogMove(string.Format("{0} gave {1} {2} to {3}.",
                    askedPlayer.Name,
                    cardsToTake.Count(),
                    cardsToTake.Count() > 1 ? Card.Plural(rank) : rank.ToString(),
                    askingPlayer.Name));
            }
            else
            {
                var rng = new RNGCryptoServiceProvider();
                var buffer = new byte[4];

                rng.GetBytes(buffer);
                var result = BitConverter.ToInt32(buffer, 0);
                var random = new Random(result).Next(Deck.Cards.Count()); 
                var cardRankPulled = GoFish(random, askingPlayer.Name);

                if (cardRankPulled.ToString().Equals(requestedRank))
                {
                    LogMove(string.Format("{0} fished a {1}.  It remains their turn.", askingPlayer.Name, cardRankPulled));
                }
                else
                {
                    CurrentPlayer = askedPlayer;
                }
            }

            PullOutBooks(askingPlayer);

            CheckForEndGame();

            return receivedCards;
        }

        /// <summary>
        ///     Takes the selected card from the pile into the players hand
        /// </summary>
        /// <param name="cardIndex"></param>
        /// <param name="playerName"></param>
        /// <exception cref="IndexOutOfRangeException">
        ///     If card index is not within the range of the pile, if playername is invalid
        /// </exception>
        public CardRank GoFish(int cardIndex, string playerName)
        {
            var askingPlayer = GetPlayer(playerName);

            var card = Deck.GetCard(cardIndex);
            askingPlayer.AddCardToHand(card); // Throws exception if index is out of range
            LogMove(string.Format("{0} went fishing.", askingPlayer.Name));

            return card.Rank;
        }

        /// <summary>
        ///     If the game is over, determines the player with the winner (most books).
        /// </summary>
        /// <returns>Players with the most books.  Null if the game is not over</returns>
        public IEnumerable<GoFishPlayer> GetWinners()
        {
            if (!IsGameOver)
                return null;
    
            var numberOfBooks = new Dictionary<string, int>();
            foreach (var book in Books)
            {
                if (numberOfBooks.ContainsKey(book.PlayerName))
                {
                    numberOfBooks[book.PlayerName] += 1;
                }
                else
                {
                    numberOfBooks.Add(book.PlayerName, 1);
                }
            }

            var highScore = numberOfBooks.OrderByDescending(p => p.Value).First().Value;
            var winners = numberOfBooks.Where(p => p.Value == highScore).Select(n => n.Key).ToList();

            if (winners.Count() == 1)
            {
                LogMove(string.Format("{0} won the game with {1} books.", winners[0], numberOfBooks[winners[0]]));
            }
            else if (winners.Count() > 1)
            {
                foreach (var playername in winners)
                {
                    LogMove(string.Format("{0} tied the game with {1} books.", playername, numberOfBooks[playername]));
                }
            }

            return winners.Select(GetPlayer).ToList();
        }

        /// <summary>
        ///     Deals five cards to each player
        /// </summary>
        private void Deal()
        {
            for (var i = 0; i < 5; i++)
            {
                foreach (var player in Players)
                    player.AddCardToHand(Deck.DealCard());
            }
        }

        /// <summary>
        ///     Checks the player's hand for books
        /// </summary>
        /// <param name="player"></param>
        /// <returns></returns>
        private void PullOutBooks(GoFishPlayer player)
        {
            var booksPulled = player.PullOutBooks();

            foreach (var rank in booksPulled)
            {
                Books.Add(new GoFishBook { PlayerName = player.Name, Rank = rank });
                LogMove(string.Format("{0} added a book of {1}", player.Name, Card.Plural(rank)));
            }
        }

        private void CheckForEndGame()
        {
            if (Deck.IsEmpty)
            {
                IsGameOver = true;
                CurrentPlayer = null;
                LogMove("Game is over because the deck is empty.");
                return;
            }

            if (Players.Any(player => !player.Hand.Any()))
            {
                IsGameOver = true;
                CurrentPlayer = null;
                var winner = Players.First(player => !player.Hand.Any());
                LogMove(string.Format("Game is over because {0}'s hand is empty.", winner.Name));
            }
        }

        private void LogMove(string description)
        {
            var move = new GoFishMove
            {
                OccurredOn = DateTime.UtcNow,
                Description = description
            };
            Moves.Add(move);
        }

        internal bool IsInProgress()
        {
            return IsGameStarted && !IsGameOver;
        }

        private static string ToUpperFirstLetter(string source)
        {
            if (string.IsNullOrEmpty(source))
                return string.Empty;
            // convert to char array of the string
            var letters = source.ToCharArray();
            // upper case the first char
            letters[0] = char.ToUpper(letters[0]);
            // return the array made of the new char array
            return new string(letters);
        }

        public bool IsTimedOut(int timeOutInMinutes)
        {
            if (!IsInProgress()) return false;

            var occurred = Moves.OrderByDescending(x => x.OccurredOn).Select(x => x.OccurredOn).First();
            var timedOut = (DateTime.UtcNow - occurred).TotalMinutes > timeOutInMinutes;
            LogMove(string.Format("Game is over because {0} hasn't played for 15 minutes.", CurrentPlayer.Name));

            IsTimeOut = true;
            IsGameOver = true;
            CurrentPlayer = null;

            return timedOut;
        }
    }
}